Skip to content

React Hooks 之设计严重依赖于闭包,使用 hooks 时容易遇到过时闭包的问题。

每一个JS模块都可以认为是一个独立的作用域,当代码执行时,该词法作用域创建执行上下文,如果在模块内部,创建了可供外部引用访问的函数时,就为闭包的产生提供了条件,只要该函数在外部执行访问了模块内部的其他变量,闭包就会产生。

JS 中闭包

常见场景

js
for ( var i=0; i<5; i++ ) {
    setTimeout(()=>{
        console.log(i)
    }, 0)
}

打印结果,都是 5。回调函数是在循环结束后才会被执行。

如何解决这种问题,一种方法就是使用闭包。

js
for ( var i=0; i<5; i++ ) {
   (function(i){
         setTimeout(()=>{
            console.log(i)
        }, 0)
   })(i)
}

定时器的回调函数去引用立即执行函数里定义的变量,形成闭包保存了立即执行函数执行时 i 的值,异步定时器的回调函数才如我们想要的打印了顺序的值。

Hooks 中的闭包

函数组件中,组件第一次渲染的时候,为每个 hook 都创建一个对象。对象中的 next 指向下一个 hook。依次如此,最终形成链表。

ts
export type Hook = {
  memoizedState: any, // 最新的状态值
  baseState: any, // 初始状态值
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null, // 环形链表,存储的是该 hook 多次调用产生的更新对象
  next: Hook | null, // next 指针,之下链表中的下一个 Hook
};

hook 中的场景:

jsx
function Counter(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
    function click(){ setCount(2) }
}

具体过程如下:

  • 第一次渲染执行 Counter,useState 设置 count 初始状态为 1。
  • 执行 useEffect ,回调函数执行,设置定时器每 s 打印 count。
  • click 触发后,调用 setCount,触发 react 更新,更新到当前组件的时候继续执行 Counter。
  • 链表已经形成,useState 将 Hook 对象上保存的状态值置为 2,count 变为 2。
  • 继续执行到 useEffect, 依赖数组为空,回调不执行。
  • 第二次更新没有涉及到定时器,定时器还在继续执行,count 仍然是第一次渲染时的值 1。count 在定时器的回调函数里被引用了,形成了闭包一直被保存。

闭包就像是一个照相机,把回调函数执行的那个时机的那些值保存了下来

解决闭包办法

方法一:

闭包中使用的变量添加到依赖项

方法二:

用函数的方式更新 state

jsx
setCount(alwaysActualStateValue => newStateValue);

useRef 每次都能拿到最新值的原因

在组件每一次渲染的过程中,ref = useRef() 返回的都是同一个对象,每次组件更新所生成的 ref 指向的都是同一片内存空间。

References

纠错与补充

  • 闭包里读到的 state 总是创建该闭包那次渲染的值,跟 setIntervalPromiserequestAnimationFrame 等异步 API 无关;真正的问题在于我们没有把“最新值”显式存放在 ref 或 reducer 中。
  • 在定时器或订阅回调里更新 state,优先使用函数式 setState(prev => ...),这样即便闭包捕获了旧的 prev,React 也会把你传入的函数放到更新队列中按顺序执行,避免“越加越慢”的假象。
  • React 19 引入的 useEvent(以及社区 useEventCallback)本质上就是对文中 useRef 模式的封装,如果是在事件处理器中读取最新值,优先采用它可以减少手写样板代码。

Copyright ©2025 moweiwei